package observer

import (
	"encoding/json"
	"fmt"
	"gitee.com/go-web-2/web_framework/context"
	"gitee.com/go-web-2/web_framework/middleware"
	"github.com/prometheus/client_golang/prometheus"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/zipkin"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	trace2 "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
	"go.opentelemetry.io/otel/trace"
	"log"
	"os"
	"strconv"
	"time"
)

func init() {
	initZipkin("", "web-server")
}

func initZipkin(collectorURL string, serviceName string) {
	exporter, err := zipkin.New(collectorURL, zipkin.WithLogger(log.New(os.Stderr, "zipkin ", log.Llongfile)))
	if err != nil {
		fmt.Printf("Error creating zipkin exporter: %v\n", err)
		return
	}
	processor := trace2.NewBatchSpanProcessor(exporter)
	tracerProvider := trace2.NewTracerProvider(trace2.WithSpanProcessor(processor), trace2.WithResource(resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceNameKey.String("zipkin-"+serviceName))))
	otel.SetTracerProvider(tracerProvider)
}

type accessLog struct {
	Host       string `json:"host"`
	Method     string `json:"method"`
	Pattern    string `json:"pattern"`
	MatchRoute string `json:"match_route"`
}

type Observer struct {
	logFunc func(str string)
	tracer  trace.Tracer
	vector  *prometheus.SummaryVec
}

func (o *Observer) setReqSpan(c *context.Context) trace.Span {
	if o.tracer == nil {
		return nil
	}
	ctx := c.R.Context()
	ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(c.R.Header))

	path := c.R.URL.Path
	var span trace.Span
	ctx, span = o.tracer.Start(ctx, path)

	span.SetAttributes(attribute.String("http.method", c.R.Method))
	span.SetAttributes(attribute.String("http.host", c.R.Host))
	span.SetAttributes(attribute.String("http.proto", c.R.Proto))
	span.SetAttributes(attribute.String("http.path", path))
	span.SetAttributes(attribute.String("http.url", c.R.RequestURI))
	span.SetAttributes(attribute.String("http.scheme", c.R.URL.Scheme))

	c.R = c.R.WithContext(ctx)
	return span
}

func (o *Observer) setRespSpan(c *context.Context, span trace.Span) {
	if o.tracer == nil {
		return
	}

	span.SetAttributes(attribute.String("http.responseStatus", strconv.Itoa(c.RespStatus)))
	span.SetAttributes(attribute.String("http.matchRoute", c.MatchRoute))
}

func (o *Observer) log(c *context.Context) {
	al := accessLog{
		Host:       c.R.Host,
		Method:     c.R.Method,
		Pattern:    c.R.RequestURI,
		MatchRoute: c.MatchRoute,
	}

	val, err := json.Marshal(al)
	if err != nil {
		o.logFunc(err.Error())
	} else {
		o.logFunc(string(val))
	}
}

func (o *Observer) setVector(c *context.Context, startTime time.Time) {
	if o.vector == nil {
		return
	}
	go func() {
		o.vector.WithLabelValues(c.R.Method, c.R.URL.Path, strconv.Itoa(c.RespStatus)).Observe(float64(time.Now().Sub(startTime).Milliseconds()))
	}()
}

func (o *Observer) Observe(next middleware.HandleFunc) middleware.HandleFunc {
	return func(c *context.Context) {
		startTime := time.Now()
		span := o.setReqSpan(c)
		defer func() {
			span.End()
			o.log(c)
			o.setVector(c, startTime)
		}()
		next(c)
		o.setRespSpan(c, span)
	}
}

type Opt func(o *Observer)

func WithLogger(logFunc func(str string)) Opt {
	return func(o *Observer) {
		o.logFunc = logFunc
	}
}

var defaultInstrumentationName = "E:\\goland\\server\\web\\middleware\\observer\\observer_test.go"

func WithTracer() Opt {
	return func(o *Observer) {
		o.tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
	}
}

func WithVector(config prometheus.SummaryOpts) Opt {
	return func(o *Observer) {
		vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
			Name:      config.Name,
			Subsystem: config.Subsystem,
			Namespace: config.Namespace,
			Help:      config.Help,
			Objectives: map[float64]float64{
				0.5:   0.01,
				0.75:  0.01,
				0.9:   0.01,
				0.99:  0.001,
				0.999: 0.0001,
			},
		}, []string{"method", "pattern", "status"})

		o.vector = vector
		prometheus.MustRegister(vector)
	}
}

func NewObserver(opts ...Opt) *Observer {
	o := &Observer{
		logFunc: func(str string) {
			fmt.Println(str)
		},
	}
	for _, op := range opts {
		op(o)
	}

	return o
}
